/*
 * Copyright 2005 The Apache Software Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.subshell.lucene.indexaccess.impl;

import java.io.IOException;
import java.util.Collections;
import java.util.EventListener;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import javax.swing.event.EventListenerList;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.search.Searcher;
import org.apache.lucene.search.Similarity;
import org.apache.lucene.store.Directory;

import com.subshell.lucene.indexaccess.ClosingSearcherEvent;
import com.subshell.lucene.indexaccess.IIndexAccessProvider;
import com.subshell.lucene.indexaccess.ILuceneIndexAccessListener;
import com.subshell.lucene.indexaccess.ILuceneIndexAccessor;
import com.subshell.lucene.indexaccess.LuceneIndexAccessEvent;

/**
 * Default ILuceneIndexAccessor implementation. It features the following
 * provisions:
 * 
 * <ul>
 * <li>Searchers and IndexReaders (read-only) will be cached and reused.</li>
 * <li>Searchers and IndexReaders (read-only) will be closed whenever an
 * IndexWriter is released. This enables subsequently opened Searchers to find
 * documents just added by the IndexWriter.</li>
 * <li>IndexReaders (read/write) and IndexWriters will be reused when accessed
 * concurrently, but will be closed when use count reaches zero.</li>
 * </ul>
 * 
 * @author Maik Schreiber (blizzy AT blizzy DOT de)
 */
public final class LuceneIndexAccessor implements ILuceneIndexAccessor {

    /** The default logger. */
    private final Log log = LogFactory.getLog(LuceneIndexAccessor.class);

    /** The index access provider. */
    private IIndexAccessProvider accessProvider;

    /** The writing reader. */
    private IndexReader cachedWritingReader = null;

    /** The writing reader use count. */
    private int writingReaderUseCount = 0;

    /** The cached writer. */
    private IndexWriter cachedWriter = null;

    /** The writer use count. */
    private int writerUseCount = 0;

    /** The reading reader use count. */
    private int readingReaderUseCount = 0;

    /** The searcher use count. */
    private int searcherUseCount = 0;

    /** The cached reading reader. */
    private IndexReader cachedReadingReader = null;

    /**
     * A map of cached servers.
     * <p>
     * SearcherConfiguration -> Searcher
     */
    private Map cachedSearchers = new HashMap();

    /** The cached reader directory. */
    private Directory cachedReadingDirectory = null;

    /** The reading directory use count. */
    private int readingDirectoryUseCount = 0;

    /** Is the writing directory in use? */
    private boolean writingDirectoryInUse = false;

    /** The writing directory. */
    private Directory writingDirectory;

    /** Is the accessor closed? */
    private boolean closed = true;

    /** The event listener list. */
    private EventListenerList listeners = new EventListenerList();

    /**
     * Default constructor.
     * 
     * @param provider
     *            The access provider.
     */
    public LuceneIndexAccessor(final IIndexAccessProvider provider) {
        this.accessProvider = provider;
    }

    /**
     * {@inheritDoc}
     */
    public IndexReader getReader(final boolean write) throws IOException {
        if (write) {
            return getWritingReader();
        } else {
            return getReadingReader();
        }
    }

    /**
     * Get the writing reader.
     * 
     * @return An index reader.
     * @throws IOException
     *             If there is an error getting the writing reader.
     */
    private IndexReader getWritingReader() throws IOException {
        IndexReader result;
        synchronized (this) {
            checkClosed();

            while ((writerUseCount > 0) || writingDirectoryInUse
                    || (readingDirectoryUseCount > 0)) {

                try {
                    wait();
                } catch (InterruptedException e) {
                    // okay
                }
            }

            closeCachedReadingDirectory();

            if (cachedWritingReader != null) {
                log.debug("returning cached writing reader");
                result = cachedWritingReader;
                writingReaderUseCount++;
            } else {
                log.debug("opening new writing reader");
                result = accessProvider.getReader();
                cachedWritingReader = result;
                writingReaderUseCount = 1;
            }

            notifyAll();
        }
        return result;
    }

    /**
     * Get an index reader.
     * 
     * @return An index reader.
     * @throws IOException
     *             If there is an error getting the reading reader.
     */
    private IndexReader getReadingReader() throws IOException {
        IndexReader result;
        synchronized (this) {
            checkClosed();

            while (writingDirectoryInUse) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    // okay
                }
            }

            if (cachedReadingReader != null) {
                log.debug("returning cached reading reader");
                result = cachedReadingReader;
                readingReaderUseCount++;
            } else {
                log.debug("opening new reading reader and caching it");
                result = accessProvider.getReader();
                cachedReadingReader = result;
                readingReaderUseCount = 1;
            }

            notifyAll();
        }
        return result;
    }

    /**
     * {@inheritDoc}
     */
    public IndexWriter getWriter() throws IOException {
        IndexWriter result;
        synchronized (this) {
            checkClosed();

            while ((writingReaderUseCount > 0) || writingDirectoryInUse
                    || (readingDirectoryUseCount > 0)) {

                try {
                    wait();
                } catch (InterruptedException e) {
                    // okay
                }
            }

            closeCachedReadingDirectory();

            if (cachedWriter != null) {
                log.debug("returning cached writer");
                result = cachedWriter;
                writerUseCount++;
            } else {
                log.debug("opening new writer and caching it");
                result = accessProvider.getWriter();
                cachedWriter = result;
                writerUseCount = 1;
            }

            notifyAll();
        }
        return result;
    }

    /**
     * {@inheritDoc}
     */
    public Searcher getSearcher() throws IOException {
        return getSearcher(Similarity.getDefault());
    }

    /**
     * {@inheritDoc}
     */
    public Searcher getSearcher(final Similarity similarity) throws IOException {
        return getSearcher(similarity, Collections.EMPTY_SET);
    }

    /**
     * {@inheritDoc}
     */
    public Searcher getSearcher(final Similarity similarity,
            final Set underlyingSearchers) throws IOException {

        Searcher result = null;
        synchronized (this) {
            checkClosed();

            while (writingDirectoryInUse) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    // okay
                }
            }

            SearcherConfiguration config = new SearcherConfiguration(
                    similarity, underlyingSearchers);
            result = (Searcher) cachedSearchers.get(config);
            if (result != null) {
                if (log.isDebugEnabled()) {
                    log.debug("returning cached searcher");
                }

                searcherUseCount++;
            } else {
                if (log.isDebugEnabled()) {
                    log.debug("opening new searcher and caching it");
                }

                result = accessProvider.getSearcher(similarity,
                        underlyingSearchers);
                cachedSearchers.put(config, result);

                searcherUseCount = 1;
            }

            notifyAll();
        }
        return result;
    }

    // public Directory getDirectory(boolean write) throws IOException {
    // return write ? getWritingDirectory() : getReadingDirectory();
    // }
    //
    // private Directory getWritingDirectory() throws IOException {
    // Directory result;
    // synchronized (this) {
    // checkClosed();
    //
    // while ((writingReaderUseCount > 0) || (writerUseCount > 0)
    // || (readingReaderUseCount > 0) || (searcherUseCount > 0)
    // || (readingDirectoryUseCount > 0) || writingDirectoryInUse) {
    //
    // try {
    // wait();
    // } catch (InterruptedException e) {
    // // okay
    // }
    // }
    //
    // closeCachedWritingReader();
    // closeCachedWriter();
    // closeCachedReadingReader();
    // closeCachedSearchers();
    // closeCachedReadingDirectory();
    //
    // log.debug("opening new writing directory");
    // result = accessProvider.getDirectory();
    //
    // writingDirectory = result;
    //
    // writingDirectoryInUse = true;
    // notifyAll();
    // }
    // return result;
    // }
    //
    // private Directory getReadingDirectory() throws IOException {
    // Directory result;
    // synchronized (this) {
    // checkClosed();
    //
    // while ((writerUseCount > 0) || (writingReaderUseCount > 0)
    // || writingDirectoryInUse) {
    //
    // try {
    // wait();
    // } catch (InterruptedException e) {
    // // okay
    // }
    // }
    //
    // closeCachedWriter();
    // closeCachedWritingReader();
    //
    // if (cachedReadingDirectory != null) {
    // log.debug("returning cached reading directory");
    // result = cachedReadingDirectory;
    // readingDirectoryUseCount++;
    // } else {
    // log.debug("opening new reading directory and caching it");
    // result = accessProvider.getDirectory();
    // cachedReadingDirectory = result;
    // readingDirectoryUseCount = 1;
    // }
    //
    // notifyAll();
    // }
    // return result;
    // }

    /**
     * {@inheritDoc}
     */
    public void release(final IndexReader reader, final boolean write) {
        if (reader != null) {
            if (write) {
                releaseWritingReader(reader);
            } else {
                releaseReadingReader(reader);
            }
        }
    }

    /**
     * Release the writing reader.
     * 
     * @param reader
     *            The writing reader to release.
     */
    private void releaseWritingReader(final IndexReader reader) {
        if (reader == null) {
            return;
        }
        synchronized (this) {
            try {
                if (reader != cachedWritingReader) {
                    throw new IllegalArgumentException(
                            "writing reader not opened by this index accessor");
                }

                writingReaderUseCount--;

                if (writingReaderUseCount == 0) {
                    log.debug("closing cached writing reader");
                    try {
                        accessProvider.close(cachedWritingReader);
                    } finally {
                        cachedWritingReader = null;
                    }
                } else {
                    log.debug("decrement cached writing reader use count");
                }
            } finally {
                notifyAll();
            }

            // make sure there are no readers before returning
            // (this is so that new readers will get our newly-added data)
            if (writingReaderUseCount == 0) {
                waitForReadersAndCloseCached();
            }
        }
    }

    /**
     * Release the reading reader.
     * 
     * @param reader
     *            The reading reader to release.
     */
    private void releaseReadingReader(final IndexReader reader) {
        if (reader == null) {
            return;
        }
        synchronized (this) {
            if (reader != cachedReadingReader) {
                throw new IllegalArgumentException(
                        "reading reader not opened by this index accessor");
            }

            readingReaderUseCount--;
            notifyAll();
        }
    }

    /**
     * {@inheritDoc}
     */
    public void release(final IndexWriter writer) {
        if (writer != null) {
            synchronized (this) {
                try {
                    if (writer != cachedWriter) {
                        throw new IllegalArgumentException(
                                "writer not opened by this index accessor");
                    }

                    writerUseCount--;

                    if (writerUseCount == 0) {
                        log.debug("closing cached writer");
                        try {
                            accessProvider.close(cachedWriter);
                        } finally {
                            cachedWriter = null;
                        }
                    } else {
                        log.debug("decrement cached writer use count");
                    }
                } finally {
                    notifyAll();
                }

                // make sure there are no readers before returning
                // (this is so that new readers will get our newly-added data)
                if (writerUseCount == 0) {
                    waitForReadersAndCloseCached();
                }
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    public void release(final Searcher searcher) {
        if (searcher != null) {
            synchronized (this) {
                if (!cachedSearchers.containsValue(searcher)) {
                    throw new IllegalArgumentException(
                            "searcher not opened by this index accessor");
                }

                searcherUseCount--;
                notifyAll();
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    public void release(final Directory directory, final boolean write) {
        if (directory != null) {
            if (write) {
                releaseWritingDirectory(directory);
            } else {
                releaseReadingDirectory(directory);
            }
        }
    }

    /**
     * Release the writing directory.
     * 
     * @param directory
     *            The writing directory to release.
     */
    private void releaseWritingDirectory(final Directory directory) {
        synchronized (this) {
            if (directory != writingDirectory) {
                throw new IllegalArgumentException(
                        "writing directory not opened by this index accessor");
            }

            try {
                log.debug("closing writing directory");
                accessProvider.close(directory);
            } finally {
                writingDirectoryInUse = false;
                writingDirectory = null;
            }

            // no need to wait for readers here since we're closing them in
            // getWritingDirectory()
        }
    }

    /**
     * Release the reading directory.
     * 
     * @param directory
     *            The writing directory to release.
     */
    private void releaseReadingDirectory(final Directory directory) {
        if (directory == null) {
            return;
        }
        synchronized (this) {
            try {
                if (directory != cachedReadingDirectory) {
                    throw new IllegalArgumentException(
                            "reading directory not opened by this index accessor");
                }

                readingDirectoryUseCount--;

                if (readingDirectoryUseCount == 0) {
                    log.debug("closing cached reading directory");
                    try {
                        accessProvider.close(cachedReadingDirectory);
                    } finally {
                        cachedReadingDirectory = null;
                    }
                }
            } finally {
                notifyAll();
            }
        }
    }

    /**
     * Wait for readers and close cached objects.
     * <p>
     * note: this method is invoked in a synchronised context
     */
    protected void waitForReadersAndCloseCached() {
        while ((readingReaderUseCount > 0) || (searcherUseCount > 0)) {
            try {
                wait();
            } catch (InterruptedException e) {
                // okay
            }
        }

        closeCachedReadingReader();
        closeCachedSearchers();
        closeCachedReadingDirectory();
    }

    /**
     * {@inheritDoc}
     */
    public void close() {
        synchronized (this) {
            if (closed) {
                throw new IllegalStateException(
                        "index accessor is already closed");
            }

            while ((readingReaderUseCount > 0) || (searcherUseCount > 0)
                    || (writingReaderUseCount > 0) || (writerUseCount > 0)
                    || (readingDirectoryUseCount > 0) || writingDirectoryInUse) {

                try {
                    wait();
                } catch (InterruptedException e) {
                    // okay
                }
            }

            closeCachedReadingReader();
            closeCachedSearchers();
            closeCachedReadingDirectory();
            closeCachedWritingReader();
            closeCachedWriter();

            closed = true;
        }
    }

    /**
     * {@inheritDoc}
     */
    public void open() {
        synchronized (this) {
            if (!closed) {
                throw new IllegalStateException(
                        "index accessor is already open");
            }
            closed = false;
        }
    }

    /**
     * {@inheritDoc}
     */
    public boolean isOpen() {
        synchronized (this) {
            return !closed;
        }
    }

    // 
    /**
     * Check is closed.
     * <p>
     * Note: this method is invoked in a synchronised context
     */
    private void checkClosed() {
        if (closed) {
            throw new IllegalStateException("index accessor has been closed");
        }
    }

    /**
     * Close the cached reading directory.
     * <p>
     * Note: this method is invoked in a synchronised context
     */
    private void closeCachedReadingDirectory() {
        if (cachedReadingDirectory != null) {
            log.debug("closing cached reading directory");
            try {
                accessProvider.close(cachedReadingDirectory);
            } catch (RuntimeException e) {
                log.error("", e);
            } finally {
                cachedReadingDirectory = null;
            }
        }
    }

    /**
     * Close the cached searchers.
     * <p>
     * Note: this method is invoked in a synchronised context
     */
    private void closeCachedSearchers() {
        if (log.isDebugEnabled()) {
            log.debug("closing cached searchers (" + cachedSearchers.size()
                    + ")");
        }

        for (Iterator i = cachedSearchers.values().iterator(); i.hasNext();) {
            Searcher searcher = (Searcher) i.next();
            fireClosingSearcher(searcher);
            try {
                accessProvider.close(searcher);
            } catch (RuntimeException e) {
                log.error("", e);
            }
        }

        cachedSearchers.clear();
    }

    /**
     * Close the cached reading reader.
     * <p>
     * Note: this method is invoked in a synchronised context
     */
    private void closeCachedReadingReader() {
        if (cachedReadingReader != null) {
            log.debug("closing cached reading reader");
            try {
                accessProvider.close(cachedReadingReader);
            } catch (RuntimeException e) {
                log.error("", e);
            } finally {
                cachedReadingReader = null;
            }
        }
    }

    /**
     * Close the cached writer.
     * <p>
     * Note: this method is invoked in a synchronised context
     */
    private void closeCachedWriter() {
        if (cachedWriter != null) {
            log.debug("closing cached writer");
            try {
                accessProvider.close(cachedWriter);
            } catch (RuntimeException e) {
                log.error("", e);
            } finally {
                cachedWriter = null;
            }
        }
    }

    /**
     * Close the cached writing reader.
     * <p>
     * Note: this method is invoked in a synchronised context
     */
    private void closeCachedWritingReader() {
        if (cachedWritingReader != null) {
            log.debug("closing cached writing reader");
            try {
                accessProvider.close(cachedWritingReader);
            } catch (RuntimeException e) {
                log.error("", e);
            } finally {
                cachedWritingReader = null;
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    public void addListener(final ILuceneIndexAccessListener listener) {
        listeners.add(ILuceneIndexAccessListener.class, listener);
    }

    /**
     * {@inheritDoc}
     */
    public void removeListener(final ILuceneIndexAccessListener listener) {
        listeners.remove(ILuceneIndexAccessListener.class, listener);
    }

    /**
     * Fire the event listeners.
     * 
     * @param searcher
     *            The searcher which is being closed.
     */
    private void fireClosingSearcher(final Searcher searcher) {
        EventListener[] eventListeners = listeners
                .getListeners(ILuceneIndexAccessListener.class);
        LuceneIndexAccessEvent event = new ClosingSearcherEvent(this, searcher);
        for (int i = 0; i < eventListeners.length; i++) {
            try {
                ((ILuceneIndexAccessListener) eventListeners[i])
                        .closingSearcher(event);
            } catch (RuntimeException e) {
                log.warn("", e);
            }
        }
    }
}
